home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 7684 / 7684.xpi / resources / fmRemote.js < prev    next >
Text File  |  2009-11-20  |  54KB  |  1,526 lines

  1. /**
  2.  * Copyright (c) 2008, Jose Enrique Bolanos, Jorge Villalobos
  3.  * All rights reserved.
  4.  *
  5.  * Redistribution and use in source and binary forms, with or without
  6.  * modification, are permitted provided that the following conditions are met:
  7.  *
  8.  *  * Redistributions of source code must retain the above copyright notice,
  9.  *    this list of conditions and the following disclaimer.
  10.  *  * Redistributions in binary form must reproduce the above copyright notice,
  11.  *    this list of conditions and the following disclaimer in the documentation
  12.  *    and/or other materials provided with the distribution.
  13.  *  * Neither the name of Jose Enrique Bolanos, Jorge Villalobos nor the names
  14.  *    of its contributors may be used to endorse or promote products derived
  15.  *    from this software without specific prior written permission.
  16.  *
  17.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  18.  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  19.  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  20.  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
  21.  * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  22.  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  23.  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  24.  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  25.  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  26.  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27.  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28.  **/
  29.  
  30. var EXPORTED_SYMBOLS = [];
  31.  
  32. const Cc = Components.classes;
  33. const Ci = Components.interfaces;
  34. const Ce = Components.Exception;
  35.  
  36. Components.utils.import("resource://firefm/fmCommon.js");
  37. Components.utils.import("resource://firefm/fmLogin.js");
  38.  
  39.  
  40. /********************** Start API 2.0 ************************/
  41.  
  42. // Scrobble client information.
  43. const SCROBBLE_CLIENT_ID = "ffm";
  44. const SCROBBLE_CLIENT_VERSION = "1.2";
  45.  
  46. // The amount of time necessary before a track can be marked to be Scrobbled.
  47. const SCROBBLE_TIME = 240 * 1000; // 240 seconds.
  48. // The minimum duration a track should have to be Scrobbled.
  49. const SCROBBLE_MIN_DURATION = 30 * 1000; // 30 seconds.
  50.  
  51. // Last.fm URLs.
  52. const URL_BASE = "http://www.last.fm";
  53. const URL_SEARCH = URL_BASE + "/music/?q=";
  54.  
  55. // API URLs.
  56. const URL_API_20 = "http://ws.audioscrobbler.com/2.0/";
  57. const URL_SCROBBLE_1_2 = "http://post.audioscrobbler.com:80/";
  58.  
  59. // Parameters sent to API calls.
  60.  
  61. const PARAMS_GET_SESSION =
  62.   "?method=auth.getSession&api_key=$(API_KEY)&token=$(TOKEN)&" +
  63.   "api_sig=$(API_SIG)";
  64. const PARAMS_SIG_GET_SESSION =
  65.   "api_key$(API_KEY)methodauth.getSessiontoken$(TOKEN)";
  66. const PARAMS_ARTIST_SEARCH =
  67.   "?method=artist.search&artist=$(ARTIST)&api_key=$(API_KEY)&limit=5";
  68. const PARAMS_TAG_SEARCH =
  69.   "?method=tag.search&api_key=$(API_KEY)&limit=1&tag=$(TAG)";
  70. const PARAMS_TRACK_LOVE =
  71.   "method=track.love&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  72.   "sk=$(SESSION_KEY)&track=$(TRACK)&artist=$(ARTIST)";
  73. const PARAMS_SIG_TRACK_LOVE =
  74.   "api_key$(API_KEY)artist$(ARTIST)methodtrack.lovesk$(SESSION_KEY)" +
  75.   "track$(TRACK)";
  76. const PARAMS_TRACK_BAN =
  77.   "method=track.ban&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  78.   "sk=$(SESSION_KEY)&track=$(TRACK)&artist=$(ARTIST)";
  79. const PARAMS_SIG_TRACK_BAN =
  80.   "api_key$(API_KEY)artist$(ARTIST)methodtrack.bansk$(SESSION_KEY)" +
  81.   "track$(TRACK)";
  82.  
  83. // Radio parameter strings.
  84. const PARAMS_TUNE_RADIO =
  85.   "method=radio.tune&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  86.   "sk=$(SESSION_KEY)&station=$(STATION_URL)";
  87. const PARAMS_SIG_TUNE_RADIO =
  88.   "api_key$(API_KEY)methodradio.tunesk$(SESSION_KEY)station$(STATION_URL)";
  89. const PARAMS_GET_PLAYLIST =
  90.   "method=radio.getPlaylist&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  91.   "sk=$(SESSION_KEY)&rtp=$(IS_SCROBBLING)";
  92. const PARAMS_SIG_GET_PLAYLIST =
  93.   "api_key$(API_KEY)methodradio.getPlaylistrtp$(IS_SCROBBLING)" +
  94.   "sk$(SESSION_KEY)";
  95.  
  96. // Get tags parameter strings
  97. const PARAMS_ARTIST_GET_TAGS =
  98.   "method=artist.getTags&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  99.   "sk=$(SESSION_KEY)&artist=$(ARTIST)";
  100. const PARAMS_SIG_ARTIST_GET_TAGS =
  101.   "api_key$(API_KEY)artist$(ARTIST)methodartist.getTagssk$(SESSION_KEY)";
  102. const PARAMS_TRACK_GET_TAGS =
  103.   "method=track.getTags&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  104.   "sk=$(SESSION_KEY)&track=$(TRACK)&artist=$(ARTIST)";
  105. const PARAMS_SIG_TRACK_GET_TAGS =
  106.   "api_key$(API_KEY)artist$(ARTIST)methodtrack.getTagssk$(SESSION_KEY)" +
  107.   "track$(TRACK)";
  108. const PARAMS_ALBUM_GET_TAGS =
  109.   "method=album.getTags&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  110.   "sk=$(SESSION_KEY)&album=$(ALBUM)&artist=$(ARTIST)";
  111. const PARAMS_SIG_ALBUM_GET_TAGS =
  112.   "album$(ALBUM)api_key$(API_KEY)artist$(ARTIST)methodalbum.getTags" +
  113.   "sk$(SESSION_KEY)";
  114.  
  115. // Add tags parameter strings
  116. const PARAMS_ARTIST_ADD_TAGS =
  117.   "method=artist.addTags&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  118.   "sk=$(SESSION_KEY)&artist=$(ARTIST)&tags=$(TAGS)";
  119. const PARAMS_SIG_ARTIST_ADD_TAGS =
  120.   "api_key$(API_KEY)artist$(ARTIST)methodartist.addTagssk$(SESSION_KEY)" +
  121.   "tags$(TAGS)";
  122. const PARAMS_TRACK_ADD_TAGS =
  123.   "method=track.addTags&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  124.   "sk=$(SESSION_KEY)&track=$(TRACK)&artist=$(ARTIST)&tags=$(TAGS)";
  125. const PARAMS_SIG_TRACK_ADD_TAGS =
  126.   "api_key$(API_KEY)artist$(ARTIST)methodtrack.addTagssk$(SESSION_KEY)" +
  127.   "tags$(TAGS)track$(TRACK)";
  128. const PARAMS_ALBUM_ADD_TAGS =
  129.   "method=album.addTags&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  130.   "sk=$(SESSION_KEY)&album=$(ALBUM)&artist=$(ARTIST)&tags=$(TAGS)";
  131. const PARAMS_SIG_ALBUM_ADD_TAGS =
  132.   "album$(ALBUM)api_key$(API_KEY)artist$(ARTIST)methodalbum.addTags" +
  133.   "sk$(SESSION_KEY)tags$(TAGS)";
  134.  
  135. // Remove tags parameter strings
  136. const PARAMS_ARTIST_REMOVE_TAG =
  137.   "method=artist.removeTag&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  138.   "sk=$(SESSION_KEY)&artist=$(ARTIST)&tag=$(TAG)";
  139. const PARAMS_SIG_ARTIST_REMOVE_TAG =
  140.   "api_key$(API_KEY)artist$(ARTIST)methodartist.removeTagsk$(SESSION_KEY)" +
  141.   "tag$(TAG)";
  142. const PARAMS_TRACK_REMOVE_TAG =
  143.   "method=track.removeTag&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  144.   "sk=$(SESSION_KEY)&track=$(TRACK)&artist=$(ARTIST)&tag=$(TAG)";
  145. const PARAMS_SIG_TRACK_REMOVE_TAG =
  146.   "api_key$(API_KEY)artist$(ARTIST)methodtrack.removeTagsk$(SESSION_KEY)" +
  147.   "tag$(TAG)track$(TRACK)";
  148. const PARAMS_ALBUM_REMOVE_TAG =
  149.   "method=album.removeTag&api_key=$(API_KEY)&api_sig=$(API_SIG)&" +
  150.   "sk=$(SESSION_KEY)&album=$(ALBUM)&artist=$(ARTIST)&tag=$(TAG)";
  151. const PARAMS_SIG_ALBUM_REMOVE_TAG =
  152.   "album$(ALBUM)api_key$(API_KEY)artist$(ARTIST)methodalbum.removeTag" +
  153.   "sk$(SESSION_KEY)tag$(TAG)";
  154.  
  155. // Scrobble parameter strings.
  156. const PARAMS_SCROBBLE_HANDSHAKE =
  157.   "?hs=true&p=1.2.1&c=" + encodeURIComponent(SCROBBLE_CLIENT_ID) + "&v=" +
  158.   encodeURIComponent(SCROBBLE_CLIENT_VERSION) + "&u=$(USER)&t=$(TIMESTAMP)" +
  159.   "&a=$(AUTH)&api_key=$(API_KEY)&sk=$(SESSION_KEY)";
  160. const PARAMS_SCROBBLE_PLAYING =
  161.   "s=$(SCROBBLE_KEY)&a=$(ARTIST)&t=$(TRACK)&b=$(ALBUM)&l=&n=&m=";
  162. // Parameters for the Scrobble POST.
  163. const PARAMS_SCROBBLE_SUBMIT =
  164.   [ "s", "a[0]", "t[0]", "b[0]", "o[0]", "m[0]", "n[0]", "l[0]", "i[0]",
  165.     "r[0]" ];
  166.  
  167. // Sourceforge URL used to submit automated error reports.
  168. const URL_SOURCEFORGE_SUBMIT =
  169.     "https://sourceforge.net/tracker/?group_id=226773&atid=1120935&" +
  170.     "func=postadd&category_id=100&artifact_group_id=100&summary=$(SUMMARY)&" +
  171.     "details=$(DETAILS)&submit=SUBMIT";
  172.  
  173.  
  174. /********************** End API 2.0 *************************/
  175.  
  176. // Regex used to obtained the embedded video inside a last.fm track page
  177. const REGEX_TRACK_VIDEO = /<div id="?lfmEmbed_(\n|.)+?<\/div>/gi;
  178.  
  179. // TODO: Is this still needed?
  180. // Last.fm URLs.
  181. const URL_EXT_BASE = "http://ext.last.fm";
  182.  
  183. /**
  184.  * Handles most of the communication with the Last.FM API. See FireFM.Login for
  185.  * other site interactions.
  186.  */
  187. FireFM.Remote = {
  188.   // Topic notifications sent from this object.
  189.   get TOPIC_TRACK_LOVED() { return "firefm-track-loved"; },
  190.  
  191.   /* Home URL. */
  192.   get URL_HOME() { return URL_BASE; },
  193.   /* Scrobble Help URL. */
  194.   get URL_SCROBBLE_HELP() {
  195.     return "http://firefm.sourceforge.net/help/#scrobbling"; },
  196.   /* Mouse gestures Help URL. */
  197.   get URL_GESTURES_HELP() {
  198.     return "http://firefm.sourceforge.net/help/#gestures"; },
  199.  
  200.   // Tag type constants
  201.   get TAG_TYPE_ARTIST() { return 0; },
  202.   get TAG_TYPE_TRACK()  { return 1; },
  203.   get TAG_TYPE_ALBUM()  { return 2; },
  204.  
  205.   /* Login Manager service reference. */
  206.   _loginManager : null,
  207.   /* Logger for this object. */
  208.   _logger : null,
  209.   /* Scrobble preference object. */
  210.   _scrobblePref : null,
  211.   /* Indicates if Scrobble is currently active. */
  212.   _scrobbleActive : false,
  213.   /* The Scrobble session ID. */
  214.   _scrobbleSessionId : null,
  215.   /* The URL used to send Now Playing information. */
  216.   _scrobbleURLNowPlaying : null,
  217.   /* The URL used to send Scrobble information. */
  218.   _scrobbleURLSubmit : null,
  219.   /* Stored Last.fm logins. It's a user/hash mapping. */
  220.   _lastFMLogins : null,
  221.   /* Holds the next track to be Scrobbled, if any. */
  222.   _toBeScrobbled : null,
  223.   /* Indicates if the current track has been loved or not. */
  224.   _loved : false,
  225.   /* Indicates if the current track has been banned or not. */
  226.   _banned : false,
  227.   /* Indicates if the current track was skipped or not. */
  228.   _skipped : false,
  229.  
  230.   /**
  231.    * Initializes this object.
  232.    */
  233.   init : function() {
  234.     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  235.     let that = this;
  236.  
  237.     this._logger = FireFM.getLogger("FireFM.Remote");
  238.     this._logger.debug("init");
  239.  
  240.     this._loginManager =
  241.       Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
  242.  
  243.     this._scrobblePref =
  244.       FireFM.Application.prefs.get(FireFM.PREF_BRANCH + "scrobble");
  245.  
  246.     // set the current value of the Scrobble preference.
  247.     this._scrobbleActive = (true == this._scrobblePref.value);
  248.     // add preference listener for the Scrobble preference.
  249.     this._scrobblePref.events.addListener("change", this);
  250.  
  251.     FireFM.obsService.addObserver(
  252.       this, FireFM.Player.TOPIC_TRACK_LOADED, false);
  253.   },
  254.  
  255.   /**
  256.    * Performs the auth.getSession call to the API. See:
  257.    * http://www.last.fm/api/show?service=125
  258.    * @param aToken the authentication token used to fetch the session.
  259.    */
  260.   authGetSession : function(aToken) {
  261.     this._logger.debug("authGetSession");
  262.  
  263.     let that = this;
  264.     let paramsSig = PARAMS_SIG_GET_SESSION;
  265.     let params = PARAMS_GET_SESSION;
  266.  
  267.     // set parameters.
  268.     paramsSig = paramsSig.replace(/\$\(API_KEY\)/, FireFM.Secret.API_KEY);
  269.     params =
  270.       params.replace(
  271.         /\$\(API_KEY\)/, encodeURIComponent(FireFM.Secret.API_KEY));
  272.     paramsSig = paramsSig.replace(/\$\(TOKEN\)/, aToken);
  273.     params = params.replace(/\$\(TOKEN\)/, encodeURIComponent(aToken));
  274.     // sign call.
  275.     params =
  276.       params.replace(
  277.         /\$\(API_SIG\)/, FireFM.Secret.generateSignature(paramsSig));
  278.  
  279.     this._sendRequest(
  280.       URL_API_20 + params,
  281.       function(aEvent) { that._authGetSessionLoad(aEvent); },
  282.       function(aEvent) { that._defaultError("authGetSession", aEvent); },
  283.       null, false, null);
  284.   },
  285.  
  286.   /**
  287.    * Load callback handler for the auth.getSession call.
  288.    * @param aEvent the event that triggered this function.
  289.    */
  290.   _authGetSessionLoad : function(aEvent) {
  291.     this._logger.trace("_authGetSessionLoad");
  292.  
  293.     try {
  294.       let doc = aEvent.target.responseXML;
  295.       let keyNode;
  296.  
  297.       if ("ok" == doc.documentElement.getAttribute("status")) {
  298.         keyNode = doc.getElementsByTagName("key")[0];
  299.         FireFM.Login.apiSession = keyNode.textContent;
  300.         // immediately request a Scrobble session.
  301.         this.scrobbleHandshake();
  302.       } else {
  303.         this._logger.error(
  304.           "_authGetSessionLoad. Invalid data received: " +
  305.           aEvent.target.responseText);
  306.         FireFM.obsService.notifyObservers(
  307.           null, FireFM.Login.TOPIC_USER_AUTHENTICATION, null);
  308.       }
  309.     } catch (e) {
  310.       this._logger.error(
  311.         "_authGetSessionLoad. Invalid data received: " +
  312.         aEvent.target.responseText + "\nError:\n" + e);
  313.       FireFM.obsService.notifyObservers(
  314.         null, FireFM.Login.TOPIC_USER_AUTHENTICATION, null);
  315.     }
  316.   },
  317.  
  318.   /**
  319.    * Performs the artist.search call to the API. See:
  320.    * http://www.last.fm/api/show?service=272
  321.    * @param aArtistName the name of the artist to search.
  322.    * @param aCallback the callback function used to return artist information
  323.    * back to the caller. This callback gets an object with {success, result}.
  324.    * 2 results are possible: {true, 'Artist'}, {false, 'URLtoSearchPage'}.
  325.    * Note that the artist in the response can be slightly different from the one
  326.    * given as a parameter. This is because Last.fm corrects common errors such
  327.    * as 'Slipnot' -> 'Slipknot'.
  328.    */
  329.   artistSearch : function(aArtistName, aCallback) {
  330.     this._logger.debug("artistSearch");
  331.  
  332.     if (("string" != typeof(aArtistName)) || (0 == aArtistName.length) ||
  333.         ("function" != typeof(aCallback))) {
  334.       throw "Invalid artist name or callback function.";
  335.     }
  336.  
  337.     let that = this;
  338.     let params = PARAMS_ARTIST_SEARCH;
  339.     let apiKey = encodeURIComponent(FireFM.Secret.API_KEY);
  340.  
  341.     // set parameters.
  342.     params = params.replace(/\$\(API_KEY\)/, apiKey);
  343.     params = params.replace(/\$\(ARTIST\)/, encodeURIComponent(aArtistName));
  344.  
  345.     this._sendRequest(
  346.       URL_API_20 + params,
  347.       function(aEvent) {
  348.         that._artistSearchLoad(aEvent, aArtistName, aCallback); },
  349.       function(aEvent) {
  350.         aCallback(
  351.           { success : false, result : that._getSearchURL(aArtistName) }); },
  352.       null, false, null);
  353.   },
  354.  
  355.   /**
  356.    * Load callback handler for the artist.search call.
  357.    * @param aEvent the event that triggered this function.
  358.    * @param aArtistName the name of the artist to search.
  359.    * @param aCallback the callback function used to return artist information
  360.    * back to the caller.
  361.    */
  362.   _artistSearchLoad : function(aEvent, aArtistName, aCallback) {
  363.     this._logger.trace("_artistSearchLoad");
  364.  
  365.     try {
  366.       let doc = aEvent.target.responseXML;
  367.       let nameNodes = doc.getElementsByTagName("name");
  368.  
  369.       if ((null != nameNodes) && (0 < nameNodes.length)) {
  370.         // default to the first result.
  371.         let artist = nameNodes[0].textContent;
  372.         let name;
  373.  
  374.         // but also look for an exact match.
  375.         for (let i = 0; i < nameNodes.length; i++) {
  376.           name = nameNodes[i].textContent;
  377.  
  378.           if (aArtistName.toLowerCase() == name.toLowerCase()) {
  379.             artist = name;
  380.             break;
  381.           }
  382.         }
  383.  
  384.         aCallback({ success : true, result : artist });
  385.       } else {
  386.         aCallback(
  387.           { success : false, result : this._getSearchURL(aArtistName) });
  388.       }
  389.     } catch (e) {
  390.       this._logger.error(
  391.         "_artistSearchLoad. Invalid data received: " +
  392.         aEvent.target.responseText + "\nError:\n" + e);
  393.       aCallback(
  394.         { success : false, result : this._getSearchURL(aArtistName) });
  395.     }
  396.   },
  397.  
  398.   /**
  399.    * Performs the radio.tune call to the API using the station the user
  400.    * specified.
  401.    * See: http://www.last.fm/api/show?service=160
  402.    */
  403.   tuneRadio : function() {
  404.     this._logger.debug("tuneRadio");
  405.  
  406.     if (null != FireFM.Login.apiSession) {
  407.       let that = this;
  408.       let params = PARAMS_TUNE_RADIO;
  409.       let paramsSig = PARAMS_SIG_TUNE_RADIO;
  410.  
  411.       let apiKey = FireFM.Secret.API_KEY;
  412.       let sessionKey = FireFM.Login.apiSession;
  413.       let stationURL = FireFM.Station.station.getStationURL();
  414.  
  415.       // set parameters to obtain signature
  416.       paramsSig = paramsSig.replace(/\$\(API_KEY\)/, apiKey);
  417.       paramsSig = paramsSig.replace(/\$\(SESSION_KEY\)/, sessionKey);
  418.       paramsSig = paramsSig.replace(/\$\(STATION_URL\)/, stationURL);
  419.  
  420.       // set parameters
  421.       params = params.replace(/\$\(API_KEY\)/, encodeURIComponent(apiKey));
  422.       params =
  423.         params.replace(/\$\(SESSION_KEY\)/,encodeURIComponent(sessionKey));
  424.       params =
  425.         params.replace(/\$\(STATION_URL\)/, encodeURIComponent(stationURL));
  426.       params =
  427.         params.replace(
  428.           /\$\(API_SIG\)/, FireFM.Secret.generateSignature(paramsSig));
  429.  
  430.       this._sendRequest(
  431.         URL_API_20 + "?" + params,
  432.         function(aEvent) { that._tuneRadioLoad(aEvent); },
  433.         function(aEvent) { that._tuneRadioError(aEvent); },
  434.         null, true, this._convertToStream(params));
  435.     } else {
  436.       this._logger.error("tuneRadio. Attempting to play music anonymously.");
  437.     }
  438.   },
  439.  
  440.   /**
  441.    * Load callback handler for the tune radio request.
  442.    * @param aEvent the event that triggered this function.
  443.    */
  444.   _tuneRadioLoad : function(aEvent) {
  445.     this._logger.trace("_tuneRadioLoad");
  446.  
  447.     try {
  448.       let doc = aEvent.target.responseXML;
  449.  
  450.       if ("ok" == doc.documentElement.getAttribute("status")) {
  451.         this._logger.trace("_tuneRadioLoad. Success");
  452.         this.getPlaylist();
  453.       } else {
  454.         this._handleRadioError(doc.getElementsByTagName("error")[0]);
  455.       }
  456.     } catch (e) {
  457.       this._logger.error(
  458.         "_tuneRadioLoad. Invalid data received: " +
  459.         aEvent.target.responseText + "\nError:\n" + e);
  460.       FireFM.obsService.notifyObservers(
  461.         FireFM.Station.station, FireFM.Station.TOPIC_STATION_ERROR,
  462.         FireFM.Station.ERROR_COMMUNICATION_FAILED);
  463.     }
  464.   },
  465.  
  466.   /**
  467.    * Error callback handler for the tune radio request.
  468.    * @param aEvent the event that triggered this function.
  469.    */
  470.   _tuneRadioError : function(aEvent) {
  471.     this._logger.error("_tuneRadioError");
  472.     FireFM.obsService.notifyObservers(
  473.       FireFM.Station.station, FireFM.Station.TOPIC_STATION_ERROR,
  474.       FireFM.Station.ERROR_COMMUNICATION_FAILED);
  475.     this._defaultError("tuneRadio", aEvent);
  476.   },
  477.  
  478.   /**
  479.    * Performs the radio.getPlaylist call to the API.
  480.    * See: http://www.last.fm/api/show?service=256
  481.    */
  482.   getPlaylist : function() {
  483.     this._logger.debug("getPlaylist");
  484.  
  485.     if (null != FireFM.Login.apiSession) {
  486.       let that = this;
  487.       let params = PARAMS_GET_PLAYLIST;
  488.       let paramsSig = PARAMS_SIG_GET_PLAYLIST;
  489.  
  490.       let apiKey = FireFM.Secret.API_KEY;
  491.       let sessionKey = FireFM.Login.apiSession;
  492.       let isScrobbling = (this._scrobbleActive && !FireFM.Private.isPrivate);
  493.  
  494.       // set parameters to obtain signature
  495.       paramsSig = paramsSig.replace(/\$\(API_KEY\)/, apiKey);
  496.       paramsSig = paramsSig.replace(/\$\(SESSION_KEY\)/, sessionKey);
  497.       paramsSig = paramsSig.replace(/\$\(IS_SCROBBLING\)/, isScrobbling);
  498.  
  499.       // set parameters
  500.       params = params.replace(/\$\(API_KEY\)/, encodeURIComponent(apiKey));
  501.       params =
  502.         params.replace(/\$\(SESSION_KEY\)/,encodeURIComponent(sessionKey));
  503.       params =
  504.         params.replace(/\$\(IS_SCROBBLING\)/, encodeURIComponent(isScrobbling));
  505.       params =
  506.         params.replace(
  507.           /\$\(API_SIG\)/, FireFM.Secret.generateSignature(paramsSig));
  508.  
  509.       this._sendRequest(
  510.         URL_API_20 + "?" + params,
  511.         function(aEvent) { that._getPlaylistLoad(aEvent); },
  512.         function(aEvent) { that._getPlaylistError(aEvent); },
  513.         null, true, this._convertToStream(params));
  514.     } else {
  515.       // use the old API if the user is not logged in.
  516.       FireFM.Login.getPlaylist();
  517.     }
  518.   },
  519.  
  520.   /**
  521.    * Load callback handler for the get playlist request.
  522.    * @param aEvent the event that triggered this function.
  523.    */
  524.   _getPlaylistLoad : function(aEvent) {
  525.     this._logger.trace("_getPlaylistLoad");
  526.  
  527.     try {
  528.       let doc = aEvent.target.responseXML;
  529.  
  530.       if ("ok" == doc.documentElement.getAttribute("status")) {
  531.         this._logger.trace("_getPlaylistLoad. Success");
  532.         FireFM.Station.loadPlaylist(doc);
  533.       } else {
  534.         this._handleRadioError(doc.getElementsByTagName("error")[0]);
  535.       }
  536.     } catch (e) {
  537.       this._logger.error(
  538.         "_getPlaylistLoad. Invalid data received: " +
  539.         aEvent.target.responseText + "\nError:\n" + e);
  540.       FireFM.obsService.notifyObservers(
  541.         FireFM.Station.station, FireFM.Station.TOPIC_STATION_ERROR,
  542.         FireFM.Station.ERROR_COMMUNICATION_FAILED);
  543.     }
  544.   },
  545.  
  546.   /**
  547.    * Error callback handler for the get playlist request.
  548.    * @param aEvent the event that triggered this function.
  549.    */
  550.   _getPlaylistError : function(aEvent) {
  551.     this._logger.error("_getPlaylistError");
  552.     FireFM.obsService.notifyObservers(
  553.       FireFM.Station.station, FireFM.Station.TOPIC_STATION_ERROR,
  554.       FireFM.Station.ERROR_COMMUNICATION_FAILED);
  555.     this._defaultError("getPlaylist", aEvent);
  556.   },
  557.  
  558.   /**
  559.    * Handles a radio error response.
  560.    * @param aErrorNode the error node received in the error response.
  561.    */
  562.   _handleRadioError : function(aErrorNode) {
  563.     this._logger.error("_handleRadioError. Error:\n " + aErrorNode.textContent);
  564.  
  565.     let error = FireFM.Station.ERROR_COMMUNICATION_FAILED;
  566.     let errorCode = aErrorNode.getAttribute("code");
  567.  
  568.     switch (errorCode) {
  569.       case "8":
  570.       case "11":
  571.         error = FireFM.Station.ERROR_SERVICE_OFFLINE;
  572.         break;
  573.       case "12":
  574.         error = FireFM.Station.ERROR_NO_SUBSCRIPTION;
  575.         break;
  576.       case "18":
  577.         error = FireFM.Station.ERROR_NO_FREE_PLAYS;
  578.         break;
  579.     }
  580.  
  581.     FireFM.obsService.notifyObservers(
  582.       FireFM.Station.station, FireFM.Station.TOPIC_STATION_ERROR, error);
  583.   },
  584.  
  585.   /**
  586.    * Performs the tag.search call to the API. See:
  587.    * http://www.last.fm/api/show?service=273
  588.    * @param aTagName the name of the tag to search.
  589.    * @param aCallback the callback function used to return tag information back
  590.    * to the caller. This callback gets an object with {success, result}.
  591.    * 2 results are possible: {true, 'tag'}, {false, 'URLtoSearchPage'}.
  592.    * Note that the tag in the response can be slightly different from the one
  593.    * given as a parameter. This is because Last.fm corrects common errors such
  594.    * as 'Slipnot' -> 'Slipknot'.
  595.    */
  596.   tagSearch : function(aTagName, aCallback) {
  597.     this._logger.debug("tagSearch");
  598.  
  599.     if (("string" != typeof(aTagName)) || (0 == aTagName.length) ||
  600.         ("function" != typeof(aCallback))) {
  601.       throw "Invalid tag name or callback function.";
  602.     }
  603.  
  604.     let that = this;
  605.     let params = PARAMS_TAG_SEARCH;
  606.     let apiKey = encodeURIComponent(FireFM.Secret.API_KEY);
  607.  
  608.     // set parameters.
  609.     params = params.replace(/\$\(API_KEY\)/, apiKey);
  610.     params = params.replace(/\$\(TAG\)/, encodeURIComponent(aTagName));
  611.  
  612.     this._sendRequest(
  613.       URL_API_20 + params,
  614.       function(aEvent) { that._tagSearchLoad(aEvent, aTagName, aCallback); },
  615.       function(aEvent) {
  616.         aCallback(
  617.           { success : false, result : that._getSearchURL(aTagName, true) }); },
  618.       null, false, null);
  619.   },
  620.  
  621.   /**
  622.    * Load callback handler for the tag.search call.
  623.    * @param aEvent the event that triggered this function.
  624.    * @param aTagName the name of the tag to search.
  625.    * @param aCallback the callback function used to return tag information back
  626.    * to the caller.
  627.    */
  628.   _tagSearchLoad : function(aEvent, aTagName, aCallback) {
  629.     this._logger.trace("_tagSearchLoad");
  630.  
  631.     try {
  632.       let doc = aEvent.target.responseXML;
  633.       let nameNodes = doc.getElementsByTagName("name");
  634.  
  635.       if ((null != nameNodes) && (0 < nameNodes.length)) {
  636.         let tag = nameNodes[0].textContent;
  637.  
  638.         aCallback({ success : true, result : tag });
  639.       } else {
  640.         aCallback(
  641.           { success : false, result : this._getSearchURL(aTagName, true) });
  642.       }
  643.     } catch (e) {
  644.       this._logger.error(
  645.         "_tagSearchLoad. Invalid data received: " +
  646.         aEvent.target.responseText + "\nError:\n" + e);
  647.       aCallback(
  648.         { success : false, result : this._getSearchURL(aTagName, true) });
  649.     }
  650.   },
  651.  
  652.   /**
  653.    * Generates a search URL for the given string.
  654.    * @param aString the query given by the user.
  655.    * @param aIsTag true if the search URL should be for a tag.
  656.    */
  657.   _getSearchURL : function(aString, aIsTag) {
  658.     this._logger.trace("_getSearchURL");
  659.  
  660.     let url =
  661.       URL_SEARCH + FireFM.encodeFMString(aString) + (aIsTag ? "&m=tags" : "");
  662.  
  663.     return url;
  664.   },
  665.  
  666.   /**
  667.    * Performs the artist.getTags, track.getTags or album.getTags calls to the
  668.    * API, using the given track. The API method called depends on the given tag
  669.    * type. See:
  670.    * http://www.last.fm/api/show?service=318
  671.    * http://www.last.fm/api/show?service=320
  672.    * http://www.last.fm/api/show?service=317
  673.    * @param aTrack The track object to which tags will be added.
  674.    * @param aTagType The type of tag to be added: artist, track or album.
  675.    * @param aCallback The method to be called when the response is received.
  676.    */
  677.   getTags : function(aTrack, aTagType, aCallback) {
  678.     this._logger.debug("getTags");
  679.  
  680.     let that = this;
  681.     let params;
  682.     let paramsSig;
  683.  
  684.     let apiKey = FireFM.Secret.API_KEY;
  685.     let sessionKey = FireFM.Login.apiSession;
  686.  
  687.     switch (aTagType) {
  688.       case this.TAG_TYPE_ARTIST:
  689.         params = PARAMS_ARTIST_GET_TAGS;
  690.         paramsSig = PARAMS_SIG_ARTIST_GET_TAGS;
  691.         break;
  692.       case this.TAG_TYPE_TRACK:
  693.         params = PARAMS_TRACK_GET_TAGS;
  694.         paramsSig = PARAMS_SIG_TRACK_GET_TAGS;
  695.         break;
  696.       case this.TAG_TYPE_ALBUM:
  697.         params = PARAMS_ALBUM_GET_TAGS;
  698.         paramsSig = PARAMS_SIG_ALBUM_GET_TAGS;
  699.         break;
  700.     }
  701.  
  702.     // set parameters to obtain signature
  703.     paramsSig = paramsSig.replace(/\$\(API_KEY\)/, apiKey);
  704.     paramsSig = paramsSig.replace(/\$\(SESSION_KEY\)/, sessionKey);
  705.     paramsSig = paramsSig.replace(/\$\(TRACK\)/, aTrack.title);
  706.     paramsSig = paramsSig.replace(/\$\(ARTIST\)/, aTrack.artist);
  707.     paramsSig = paramsSig.replace(/\$\(ALBUM\)/, aTrack.album);
  708.  
  709.     // set parameters
  710.     params = params.replace(/\$\(API_KEY\)/, encodeURIComponent(apiKey));
  711.     params =
  712.       params.replace(/\$\(SESSION_KEY\)/,encodeURIComponent(sessionKey));
  713.     params = params.replace(/\$\(TRACK\)/, encodeURIComponent(aTrack.title));
  714.     params = params.replace(/\$\(ARTIST\)/, encodeURIComponent(aTrack.artist));
  715.     params = params.replace(/\$\(ALBUM\)/, encodeURIComponent(aTrack.album));
  716.     params =
  717.       params.replace(
  718.         /\$\(API_SIG\)/, FireFM.Secret.generateSignature(paramsSig));
  719.  
  720.     this._sendRequest(
  721.       URL_API_20 + "?" + params,
  722.       function(aEvent) { that._getTagsLoad(aEvent, aCallback); },
  723.       function(aEvent) {
  724.         that._defaultError("getTags", aEvent);
  725.         aCallback([]);
  726.       },
  727.       null, true, this._convertToStream(params));
  728.   },
  729.  
  730.   /**
  731.    * Load callback handler for the getTags request.
  732.    * @param aEvent The event that triggered this function.
  733.    */
  734.   _getTagsLoad : function(aEvent, aCallback) {
  735.     this._logger.trace("_getTagsLoad");
  736.  
  737.     let tags = [];
  738.  
  739.     try {
  740.       let doc = aEvent.target.responseXML;
  741.  
  742.       if ("ok" == doc.documentElement.getAttribute("status")) {
  743.         this._logger.trace("_getTagsLoad. Success");
  744.  
  745.         let tagNodes = doc.getElementsByTagName("tag");
  746.  
  747.         for (let i = 0; i < tagNodes.length; i++) {
  748.           tags.push(tagNodes[i].getElementsByTagName("name")[0].textContent);
  749.         }
  750.  
  751.       } else {
  752.         let error = doc.getElementsByTagName("error")[0].textContent;
  753.         this._logger.error("_getTagsLoad. Failed: " + error);
  754.       }
  755.     } catch (e) {
  756.       this._logger.error(
  757.         "_getTagsLoad. Invalid data received: " +
  758.         aEvent.target.responseText + "\nError:\n" + e);
  759.     }
  760.  
  761.     aCallback(tags);
  762.   },
  763.  
  764.   /**
  765.    * Performs the artist.addTags, track.addTags or album.addTags calls to the
  766.    * API, using the given track. The API method called depends on the given tag
  767.    * type. See:
  768.    * http://www.last.fm/api/show?service=303
  769.    * http://www.last.fm/api/show?service=304
  770.    * http://www.last.fm/api/show?service=302
  771.    * @param aTrack The track object to which tags will be added
  772.    * @param aTagType The type of tag to be added: artist, track or album.
  773.    * @param aTags The array of tags to be added.
  774.    */
  775.   addTags : function(aTrack, aTagType, aTags) {
  776.     this._logger.debug("addTags");
  777.  
  778.     let that = this;
  779.     let params;
  780.     let paramsSig;
  781.  
  782.     let apiKey = FireFM.Secret.API_KEY;
  783.     let sessionKey = FireFM.Login.apiSession;
  784.     let tags = String(aTags);
  785.  
  786.     switch (aTagType) {
  787.       case this.TAG_TYPE_ARTIST:
  788.         params = PARAMS_ARTIST_ADD_TAGS;
  789.         paramsSig = PARAMS_SIG_ARTIST_ADD_TAGS;
  790.         break;
  791.       case this.TAG_TYPE_TRACK:
  792.         params = PARAMS_TRACK_ADD_TAGS;
  793.         paramsSig = PARAMS_SIG_TRACK_ADD_TAGS;
  794.         break;
  795.       case this.TAG_TYPE_ALBUM:
  796.         params = PARAMS_ALBUM_ADD_TAGS;
  797.         paramsSig = PARAMS_SIG_ALBUM_ADD_TAGS;
  798.         break;
  799.     }
  800.  
  801.     // set parameters to obtain signature
  802.     paramsSig = paramsSig.replace(/\$\(API_KEY\)/, apiKey);
  803.     paramsSig = paramsSig.replace(/\$\(SESSION_KEY\)/, sessionKey);
  804.     paramsSig = paramsSig.replace(/\$\(TRACK\)/, aTrack.title);
  805.     paramsSig = paramsSig.replace(/\$\(ARTIST\)/, aTrack.artist);
  806.     paramsSig = paramsSig.replace(/\$\(ALBUM\)/, aTrack.album);
  807.     paramsSig = paramsSig.replace(/\$\(TAGS\)/, tags);
  808.  
  809.     // set parameters
  810.     params = params.replace(/\$\(API_KEY\)/, encodeURIComponent(apiKey));
  811.     params =
  812.       params.replace(/\$\(SESSION_KEY\)/,encodeURIComponent(sessionKey));
  813.     params = params.replace(/\$\(TRACK\)/, encodeURIComponent(aTrack.title));
  814.     params = params.replace(/\$\(ARTIST\)/, encodeURIComponent(aTrack.artist));
  815.     params = params.replace(/\$\(ALBUM\)/, encodeURIComponent(aTrack.album));
  816.     params = params.replace(/\$\(TAGS\)/, encodeURIComponent(tags));
  817.     params =
  818.       params.replace(
  819.         /\$\(API_SIG\)/, FireFM.Secret.generateSignature(paramsSig));
  820.  
  821.     this._sendRequest(
  822.       URL_API_20 + "?" + params,
  823.       function(aEvent) { that._addTagsLoad(aEvent); },
  824.       function(aEvent) { that._defaultError("addTags", aEvent); },
  825.       null, true, this._convertToStream(params));
  826.   },
  827.  
  828.   /**
  829.    * Load callback handler for the addTags request.
  830.    * @param aEvent The event that triggered this function.
  831.    */
  832.   _addTagsLoad : function(aEvent) {
  833.     this._logger.trace("_addTagsLoad");
  834.  
  835.     try {
  836.       let doc = aEvent.target.responseXML;
  837.  
  838.       if ("ok" == doc.documentElement.getAttribute("status")) {
  839.         this._logger.trace("_addTagsLoad. Success");
  840.       } else {
  841.         let error = doc.getElementsByTagName("error")[0].textContent;
  842.         this._logger.error("_addTagsLoad. Failed: " + error);
  843.       }
  844.     } catch (e) {
  845.       this._logger.error(
  846.         "_addTagsLoad. Invalid data received: " +
  847.         aEvent.target.responseText + "\nError:\n" + e);
  848.     }
  849.   },
  850.  
  851.   /**
  852.    * Performs the artist.removeTag, track.removeTag or album.removeTag calls to
  853.    * the API, using the given track. The API method called depends on the given
  854.    * tag type. See:
  855.    * http://www.last.fm/api/show?service=315
  856.    * http://www.last.fm/api/show?service=316
  857.    * http://www.last.fm/api/show?service=314
  858.    * @param aTrack The track object from which the tag will be removed.
  859.    * @param aTagType The type of tag to be removed: artist, track or album.
  860.    * @param aTag The tag to be removed.
  861.    */
  862.   removeTag : function(aTrack, aTagType, aTag) {
  863.     this._logger.debug("removeTag");
  864.  
  865.     let that = this;
  866.     let params;
  867.     let paramsSig;
  868.  
  869.     let apiKey = FireFM.Secret.API_KEY;
  870.     let sessionKey = FireFM.Login.apiSession;
  871.  
  872.     switch (aTagType) {
  873.       case this.TAG_TYPE_ARTIST:
  874.         params = PARAMS_ARTIST_REMOVE_TAG;
  875.         paramsSig = PARAMS_SIG_ARTIST_REMOVE_TAG;
  876.         break;
  877.       case this.TAG_TYPE_TRACK:
  878.         params = PARAMS_TRACK_REMOVE_TAG;
  879.         paramsSig = PARAMS_SIG_TRACK_REMOVE_TAG;
  880.         break;
  881.       case this.TAG_TYPE_ALBUM:
  882.         params = PARAMS_ALBUM_REMOVE_TAG;
  883.         paramsSig = PARAMS_SIG_ALBUM_REMOVE_TAG;
  884.         break;
  885.     }
  886.  
  887.     // set parameters to obtain signature
  888.     paramsSig = paramsSig.replace(/\$\(API_KEY\)/, apiKey);
  889.     paramsSig = paramsSig.replace(/\$\(SESSION_KEY\)/, sessionKey);
  890.     paramsSig = paramsSig.replace(/\$\(TRACK\)/, aTrack.title);
  891.     paramsSig = paramsSig.replace(/\$\(ARTIST\)/, aTrack.artist);
  892.     paramsSig = paramsSig.replace(/\$\(ALBUM\)/, aTrack.album);
  893.     paramsSig = paramsSig.replace(/\$\(TAG\)/, aTag);
  894.  
  895.     // set parameters
  896.     params = params.replace(/\$\(API_KEY\)/, encodeURIComponent(apiKey));
  897.     params =
  898.       params.replace(/\$\(SESSION_KEY\)/,encodeURIComponent(sessionKey));
  899.     params = params.replace(/\$\(TRACK\)/, encodeURIComponent(aTrack.title));
  900.     params = params.replace(/\$\(ARTIST\)/, encodeURIComponent(aTrack.artist));
  901.     params = params.replace(/\$\(ALBUM\)/, encodeURIComponent(aTrack.album));
  902.     params = params.replace(/\$\(TAG\)/, encodeURIComponent(aTag));
  903.     params =
  904.       params.replace(
  905.         /\$\(API_SIG\)/, FireFM.Secret.generateSignature(paramsSig));
  906.  
  907.     this._sendRequest(
  908.       URL_API_20 + "?" + params,
  909.       function(aEvent) { that._removeTagLoad(aEvent); },
  910.       function(aEvent) { that._defaultError("removeTag", aEvent); },
  911.       null, true, this._convertToStream(params));
  912.   },
  913.  
  914.   /**
  915.    * Load callback handler for the removeTag request.
  916.    * @param aEvent The event that triggered this function.
  917.    */
  918.   _removeTagLoad : function(aEvent) {
  919.     this._logger.trace("_removeTagLoad");
  920.  
  921.     try {
  922.       let doc = aEvent.target.responseXML;
  923.  
  924.       if ("ok" == doc.documentElement.getAttribute("status")) {
  925.         this._logger.trace("_removeTagLoad. Success");
  926.       } else {
  927.         let error = doc.getElementsByTagName("error")[0].textContent;
  928.         this._logger.error("_removeTagLoad. Failed: " + error);
  929.       }
  930.     } catch (e) {
  931.       this._logger.error(
  932.         "_removeTagLoad. Invalid data received: " +
  933.         aEvent.target.responseText + "\nError:\n" + e);
  934.     }
  935.   },
  936.  
  937.   /**
  938.    * Performs the track.love call to the API using the track being played.
  939.    * See: http://www.last.fm/api/show?service=260
  940.    */
  941.   loveTrack : function() {
  942.     this._logger.debug("loveTrack");
  943.  
  944.     let that = this;
  945.     let params = PARAMS_TRACK_LOVE;
  946.     let paramsSig = PARAMS_SIG_TRACK_LOVE;
  947.  
  948.     let apiKey = FireFM.Secret.API_KEY;
  949.     let sessionKey = FireFM.Login.apiSession;
  950.     let track = FireFM.Playlist.currentTrack.title;
  951.     let artist = FireFM.Playlist.currentTrack.artist;
  952.  
  953.     // set parameters to obtain signature
  954.     paramsSig = paramsSig.replace(/\$\(API_KEY\)/, apiKey);
  955.     paramsSig = paramsSig.replace(/\$\(SESSION_KEY\)/, sessionKey);
  956.     paramsSig = paramsSig.replace(/\$\(TRACK\)/, track);
  957.     paramsSig = paramsSig.replace(/\$\(ARTIST\)/, artist);
  958.  
  959.     // set parameters
  960.     params = params.replace(/\$\(API_KEY\)/, encodeURIComponent(apiKey));
  961.     params =
  962.       params.replace(/\$\(SESSION_KEY\)/,encodeURIComponent(sessionKey));
  963.     params = params.replace(/\$\(TRACK\)/, encodeURIComponent(track));
  964.     params = params.replace(/\$\(ARTIST\)/, encodeURIComponent(artist));
  965.     params =
  966.       params.replace(
  967.         /\$\(API_SIG\)/, FireFM.Secret.generateSignature(paramsSig));
  968.  
  969.     this._sendRequest(
  970.       URL_API_20 + "?" + params,
  971.       function(aEvent) { that._loveTrackLoad(aEvent); },
  972.       function(aEvent) { that._loveTrackError(aEvent); },
  973.       null, true, this._convertToStream(params));
  974.   },
  975.  
  976.   /**
  977.    * Load callback handler for the love track request.
  978.    * @param aEvent the event that triggered this function.
  979.    */
  980.   _loveTrackLoad : function(aEvent) {
  981.     this._logger.trace("_loveTrackLoad");
  982.  
  983.     try {
  984.       let doc = aEvent.target.responseXML;
  985.  
  986.       if ("ok" == doc.documentElement.getAttribute("status")) {
  987.         this._logger.trace("_loveTrackLoad. Success");
  988.         this._loved = true;
  989.         FireFM.obsService.notifyObservers(null, this.TOPIC_TRACK_LOVED, true);
  990.       } else {
  991.         let error = doc.getElementsByTagName("error")[0].textContent;
  992.         this._logger.error("_loveTrackLoad. Failed: " + error);
  993.         FireFM.obsService.notifyObservers(null, this.TOPIC_TRACK_LOVED, false);
  994.       }
  995.     } catch (e) {
  996.       this._logger.error(
  997.         "_loveTrackLoad. Invalid data received: " +
  998.         aEvent.target.responseText + "\nError:\n" + e);
  999.       FireFM.obsService.notifyObservers(null, this.TOPIC_TRACK_LOVED, false);
  1000.     }
  1001.   },
  1002.  
  1003.   /**
  1004.    * Error callback handler for the love track request.
  1005.    * @param aEvent the event that triggered this function.
  1006.    */
  1007.   _loveTrackError : function(aEvent) {
  1008.     this._logger.error("_loveTrackError");
  1009.     FireFM.obsService.notifyObservers(null, this.TOPIC_TRACK_LOVED, false);
  1010.     this._defaultError("loveTrack", aEvent);
  1011.   },
  1012.  
  1013.   /**
  1014.    * Performs the track.ban call to the API using the track being played.
  1015.    * See: http://www.last.fm/api/show?service=260
  1016.    */
  1017.   banTrack : function() {
  1018.     this._logger.debug("banTrack");
  1019.  
  1020.     let that = this;
  1021.     let params = PARAMS_TRACK_BAN;
  1022.     let paramsSig = PARAMS_SIG_TRACK_BAN;
  1023.  
  1024.     let apiKey = FireFM.Secret.API_KEY;
  1025.     let sessionKey = FireFM.Login.apiSession;
  1026.     let track = FireFM.Playlist.currentTrack.title;
  1027.     let artist = FireFM.Playlist.currentTrack.artist;
  1028.  
  1029.     this._banned = true;
  1030.  
  1031.     // set parameters to obtain signature
  1032.     paramsSig = paramsSig.replace(/\$\(API_KEY\)/, apiKey);
  1033.     paramsSig = paramsSig.replace(/\$\(SESSION_KEY\)/, sessionKey);
  1034.     paramsSig = paramsSig.replace(/\$\(TRACK\)/, track);
  1035.     paramsSig = paramsSig.replace(/\$\(ARTIST\)/, artist);
  1036.  
  1037.     // set parameters
  1038.     params = params.replace(/\$\(API_KEY\)/, encodeURIComponent(apiKey));
  1039.     params =
  1040.       params.replace(/\$\(SESSION_KEY\)/,encodeURIComponent(sessionKey));
  1041.     params = params.replace(/\$\(TRACK\)/, encodeURIComponent(track));
  1042.     params = params.replace(/\$\(ARTIST\)/, encodeURIComponent(artist));
  1043.     params =
  1044.       params.replace(
  1045.         /\$\(API_SIG\)/, FireFM.Secret.generateSignature(paramsSig));
  1046.  
  1047.     this._sendRequest(
  1048.       URL_API_20 + "?" + params,
  1049.       function(aEvent) { that._banTrackLoad(aEvent); },
  1050.       function(aEvent) { that._banTrackError(aEvent); },
  1051.       null, true, this._convertToStream(params));
  1052.   },
  1053.  
  1054.   /**
  1055.    * Load callback handler for the ban track request.
  1056.    * @param aEvent The event that triggered this function.
  1057.    */
  1058.   _banTrackLoad : function(aEvent) {
  1059.     this._logger.trace("_banTrackLoad");
  1060.  
  1061.     try {
  1062.       let doc = aEvent.target.responseXML;
  1063.  
  1064.       if ("ok" == doc.documentElement.getAttribute("status")) {
  1065.         this._logger.trace("_banTrackLoad. Success");
  1066.       } else {
  1067.         let error = doc.getElementsByTagName("error")[0].textContent;
  1068.         this._logger.error("_banTrackLoad. Failed: " + error);
  1069.       }
  1070.     } catch (e) {
  1071.       this._logger.error(
  1072.         "_banTrackLoad. Invalid data received: " +
  1073.         aEvent.target.responseText + "\nError:\n" + e);
  1074.     }
  1075.   },
  1076.  
  1077.   /**
  1078.    * Error callback handler for the ban track request.
  1079.    * @param aEvent The event that triggered this function.
  1080.    */
  1081.   _banTrackError : function(aEvent) {
  1082.     this._logger.error("_banTrackError");
  1083.     this._defaultError("banTrack", aEvent);
  1084.   },
  1085.  
  1086.   /**
  1087.    * Sends a handshake request for to begin a Scrobble session.
  1088.    * See: http://www.last.fm/api/submissions
  1089.    * @param aCallback (optional) function to be called if the handshake is
  1090.    * successful.
  1091.    */
  1092.   scrobbleHandshake : function(aCallback) {
  1093.     this._logger.debug("scrobbleHandshake");
  1094.  
  1095.     let that = this;
  1096.     let params = PARAMS_SCROBBLE_HANDSHAKE;
  1097.     let timestamp = Math.floor(Date.now() / 1000);
  1098.     let auth =
  1099.       encodeURIComponent(FireFM.Secret.generateScrobbleAuth(timestamp));
  1100.  
  1101.     // set parameters.
  1102.     params =
  1103.       params.replace(/\$\(USER\)/, encodeURIComponent(FireFM.Login.userName));
  1104.     params = params.replace(/\$\(TIMESTAMP\)/, timestamp);
  1105.     params = params.replace(/\$\(AUTH\)/, auth);
  1106.     params =
  1107.       params.replace(
  1108.         /\$\(API_KEY\)/, encodeURIComponent(FireFM.Secret.API_KEY));
  1109.     params =
  1110.       params.replace(
  1111.         /\$\(SESSION_KEY\)/, encodeURIComponent(FireFM.Login.apiSession));
  1112.  
  1113.     this._sendRequest(
  1114.       URL_SCROBBLE_1_2 + params,
  1115.       function(aEvent) { that._scrobbleHandshakeLoad(aEvent, aCallback); },
  1116.       function(aEvent) { that._defaultError("scrobbleHandshake", aEvent); },
  1117.       null, false, null);
  1118.   },
  1119.  
  1120.   /**
  1121.    * Load callback handler for the Scrobble handshake request.
  1122.    * @param aEvent the event that triggered this function.
  1123.    * @param aCallback (optional) function to be called if the handshake is
  1124.    * successful.
  1125.    */
  1126.   _scrobbleHandshakeLoad : function(aEvent, aCallback) {
  1127.     this._logger.trace("_scrobbleHandshakeLoad");
  1128.  
  1129.     try {
  1130.       let data = aEvent.target.responseText;
  1131.       let lines = data.split("\n");
  1132.  
  1133.       // do a little integrity check.
  1134.       if ((4 <= lines.length) && ("OK" == lines[0])) {
  1135.         this._scrobbleSessionId = lines[1];
  1136.         this._scrobbleURLNowPlaying = lines[2].replace(/\:80/, "");
  1137.         this._scrobbleURLSubmit = lines[3].replace(/\:80/, "");
  1138.         FireFM.obsService.addObserver(
  1139.           this, FireFM.Player.TOPIC_PROGRESS_CHANGED, false);
  1140.         this._logger.debug("_scrobbleHandshakeLoad. Scrobble data loaded.");
  1141.  
  1142.         if (aCallback) {
  1143.           aCallback();
  1144.         }
  1145.       } else {
  1146.         this._logger.error(
  1147.           "_scrobbleHandshakeLoad. Invalid data received: " + data);
  1148.       }
  1149.     } catch (e) {
  1150.       this._logger.error(
  1151.         "_scrobbleHandshakeLoad. Invalid data received: " +
  1152.         aEvent.target.responseText + "\nError:\n" + e);
  1153.     }
  1154.   },
  1155.  
  1156.   /**
  1157.    * Sends the 'Now Playing' call to the Scrobble URL, with the information of
  1158.    * the given track.
  1159.    * See: http://www.last.fm/api/submissions#np
  1160.    * @param aTrack the track to send information about.
  1161.    */
  1162.   _sendNowPlaying : function(aTrack) {
  1163.     this._logger.debug("_sendNowPlaying");
  1164.  
  1165.     if (this._scrobbleActive && (null != this._scrobbleSessionId) &&
  1166.         !FireFM.Private.isPrivate) {
  1167.       let that = this;
  1168.       let params = PARAMS_SCROBBLE_PLAYING;
  1169.  
  1170.       params = params.replace(/\$\(SCROBBLE_KEY\)/, this._scrobbleSessionId);
  1171.       params =
  1172.         params.replace(/\$\(ARTIST\)/, encodeURIComponent(aTrack.artist));
  1173.       params = params.replace(/\$\(TRACK\)/, encodeURIComponent(aTrack.title));
  1174.       params =
  1175.         params.replace(/\$\(ALBUM\)/, encodeURIComponent(aTrack.albumTitle));
  1176.  
  1177.       this._sendRequest(
  1178.         this._scrobbleURLNowPlaying,
  1179.         function(aEvent) {
  1180.           that._logger.debug("_sendNowPlaying: " + aEvent.target.responseText);
  1181.         },
  1182.         function(aEvent) { that._defaultError("sendNowPlaying", aEvent); },
  1183.         null, true, params);
  1184.     }
  1185.   },
  1186.  
  1187.   /**
  1188.    * Sends the last track to the Scrobble service, if it should.
  1189.    * See: http://www.last.fm/api/submissions#subs
  1190.    * @param aTrack optional argument that 'forces' a specific track to be
  1191.    * Scrobbled, instead of the one in queue.
  1192.    */
  1193.   scrobbleTrack : function(aTrack) {
  1194.     this._logger.debug("scrobbleTrack");
  1195.  
  1196.     let track = (aTrack ? aTrack : this._toBeScrobbled);
  1197.  
  1198.     if (this._scrobbleActive && (null != track) && !FireFM.Private.isPrivate) {
  1199.       let that = this;
  1200.       let rating =
  1201.         (this._banned ? "B" : (this._loved ? "L" : (this._skipped ? "S" : "")));
  1202.       let postParams =
  1203.         [ this._scrobbleSessionId, track.artist, track.title, track.albumTitle,
  1204.          ("L" + track.trackAuth), "", "", track.duration, track.startTime,
  1205.          rating ];
  1206.       let postString = "";
  1207.       let inputStream;
  1208.  
  1209.       // generate the POST string.
  1210.       for (let i = 0; i < PARAMS_SCROBBLE_SUBMIT.length; i++) {
  1211.         postString +=
  1212.           (0 < i ? "&" : "") + encodeURIComponent(PARAMS_SCROBBLE_SUBMIT[i]) +
  1213.           "=" + encodeURIComponent(postParams[i]);
  1214.       }
  1215.  
  1216.       inputStream = this._convertToStream(postString);
  1217.  
  1218.       this._sendRequest(
  1219.         this._scrobbleURLSubmit,
  1220.         function(aEvent) { that._scrobbleTrackLoad(aEvent, track); },
  1221.         function(aEvent) { that._defaultError("scrobbleTrack", aEvent); },
  1222.         { "Content-Type" : "application/x-www-form-urlencoded" }, true,
  1223.         inputStream);
  1224.     }
  1225.   },
  1226.  
  1227.   /**
  1228.    * Load callback handler for the Scrobble track request.
  1229.    * @param aEvent the event that triggered this function.
  1230.    * @param aTrack the scrobbled track. In case of session error, we can
  1231.    * Scrobble again.
  1232.    */
  1233.   _scrobbleTrackLoad : function(aEvent, aTrack) {
  1234.     this._logger.trace("_scrobbleTrackLoad");
  1235.  
  1236.     try {
  1237.       let data = aEvent.target.responseText;
  1238.  
  1239.       if (0 == data.indexOf("OK")) {
  1240.         this._logger.trace("_scrobbleTrackLoad. Success");
  1241.       } else if (0 == data.indexOf("BADSESSION")) {
  1242.         let that = this;
  1243.  
  1244.         this._logger.warn("_scrobbleTrackLoad. Bad session. Reconnecting.");
  1245.  
  1246.         try {
  1247.           FireFM.obsService.removeObserver(
  1248.             this, FireFM.Player.TOPIC_PROGRESS_CHANGED);
  1249.         } catch (e) {
  1250.           this._logger.warn(
  1251.             "_scrobbleTrackLoad. Error removing observer:\n" + e);
  1252.         }
  1253.  
  1254.         // retry the handshake and run the Scrobble call if it works.
  1255.         this.scrobbleHandshake(function() { that.scrobbleTrack(aTrack); });
  1256.       } else {
  1257.         this._logger.error(
  1258.           "_scrobbleTrackLoad. Invalid data received: " + data);
  1259.       }
  1260.     } catch (e) {
  1261.       this._logger.error(
  1262.         "_scrobbleTrackLoad. Invalid data received: " +
  1263.         aEvent.target.responseText + "\nError:\n" + e);
  1264.     }
  1265.   },
  1266.  
  1267.   /**
  1268.    * Indicates if the track can be Scrobbled or not.
  1269.    * See: http://www.last.fm/api/submissions#subs
  1270.    * @param aTrack the track to check for Scrobbling.
  1271.    * @param aProgress the current progress percentage.
  1272.    * @return true if the track can be Scrobbled, false otherwise.
  1273.    */
  1274.   _canScrobble : function(aTrack, aProgress) {
  1275.     // XXX: no logging here for performance reasons.
  1276.     let canScrobble =
  1277.       ((50 <= parseInt(aProgress, 10)) ||
  1278.        (SCROBBLE_TIME <= ((aTrack.duration * aProgress) / 100)));
  1279.  
  1280.     return canScrobble;
  1281.   },
  1282.  
  1283.   /**
  1284.    * Marks the current track as 'skipped'.
  1285.    */
  1286.   skipTrack : function() {
  1287.     this._logger.debug("skipTrack");
  1288.     this._skipped = true;
  1289.   },
  1290.  
  1291.   /**
  1292.    * Obtains the feed from the specified URL, and notifies the callback handler
  1293.    * of the result.
  1294.    * @param aURL the URL to fetch the feed from.
  1295.    * @param aCallback the callback method for this call. This callback gets the
  1296.    * XMLHTTPRequest object, or null in case an error occurs.
  1297.    */
  1298.   fetchFeed : function(aURL, aCallback) {
  1299.     this._logger.debug("fetchFeed");
  1300.  
  1301.     let that = this;
  1302.  
  1303.     this._sendRequest(
  1304.       aURL, function(aEvent) { aCallback(aEvent.target); },
  1305.       function(aEvent) {
  1306.         aCallback(null); that._defaultError("fetchFeed", aEvent); },
  1307.       null, false);
  1308.   },
  1309.  
  1310.   /**
  1311.    * Sends a player load error to our Sourceforge bug tracker.
  1312.    * @para aErrorInfo string with the details of the error to be sent.
  1313.    */
  1314.   sendPlayerLoadError : function(aErrorInfo) {
  1315.     this._logger.debug("sendPlayerLoadError");
  1316.  
  1317.     let timestamp = new Date().getTime();
  1318.     let url = URL_SOURCEFORGE_SUBMIT;
  1319.     let that = this;
  1320.  
  1321.     url =
  1322.       url.replace(
  1323.         /\$\(SUMMARY\)/, encodeURIComponent("Player load error " + timestamp));
  1324.     url = url.replace(/\$\(DETAILS\)/, encodeURIComponent(aErrorInfo));
  1325.  
  1326.     this._sendRequest(
  1327.       url, function(aEvent) { that._sendPlayerLoadErrorLoad(aEvent); },
  1328.       function(aEvent) { that._defaultError("sendPlayerLoadError", aEvent); },
  1329.       { "Content-Type" : "application/x-www-form-urlencoded" }, false, null);
  1330.   },
  1331.  
  1332.   /**
  1333.    * Load callback handler for the send player load error request.
  1334.    * @param aEvent the event that triggered this function.
  1335.    */
  1336.   _sendPlayerLoadErrorLoad : function(aEvent) {
  1337.     this._logger.error(
  1338.       "_sendPlayerLoadErrorLoad. Response: " + aEvent.target.responseText);
  1339.   },
  1340.  
  1341.   /**
  1342.    * Opens the track page and looks for the embedded track video.
  1343.    * @param aTrack The track object whose video will be obtained.
  1344.    * @param aCallback The method to be called if and when a video is found.
  1345.    */
  1346.   getTrackVideo : function(aTrack, aCallback) {
  1347.     this._logger.debug("getTrackVideo");
  1348.  
  1349.     let that = this;
  1350.     this._sendRequest(
  1351.       aTrack.trackURL,
  1352.       function(aEvent) { that._getTrackVideoLoad(aTrack, aEvent, aCallback); },
  1353.       function(aEvent) { that._defaultError("getTrackVideo", aEvent); },
  1354.       null, false, null);
  1355.   },
  1356.  
  1357.   /**
  1358.    * Handles the response from opening the track page. Parses the response text
  1359.    * and looks for an embedded video. If it is found then the callback method
  1360.    * is called.
  1361.    * @param aTrack The track object whose video is being obtained.
  1362.    * @param aEvent The http request event that triggered this method.
  1363.    * @param aCallback The method to be called if and when a video is found.
  1364.    */
  1365.   _getTrackVideoLoad : function(aTrack, aEvent, aCallback) {
  1366.     this._logger.trace("_getTrackVideoLoad");
  1367.  
  1368.     try {
  1369.       let videoMatch = REGEX_TRACK_VIDEO.exec(aEvent.target.responseText);
  1370.  
  1371.       if (videoMatch) {
  1372.         aCallback(aTrack, videoMatch[0]);
  1373.       }
  1374.  
  1375.     } catch (e) {
  1376.       this._logger.error(
  1377.         "_getTrackVideoLoad. Could not parse track page to get video: " +
  1378.         aEvent.target.responseText + "\nError:\n" + e);
  1379.     }
  1380.   },
  1381.  
  1382.   /**
  1383.    * Sends an HTTP request. This is just an utility function to save some code
  1384.    * lines.
  1385.    * @param aURL the url to send the request to.
  1386.    * @param aLoadHandler the load callback handler. Can be null.
  1387.    * @param aErrorHandler the error callback handler. Can be null.
  1388.    * @param aHeaders object mapping that represents the headers to send. Can be
  1389.    * null or empty.
  1390.    * @param aIsPOST indicates if the method POST (true) or GET (false).
  1391.    * @param aPOSTString the string or stream to send through post (optional).
  1392.    */
  1393.   _sendRequest : function(
  1394.     aURL, aLoadHandler, aErrorHandler, aHeaders, aIsPOST, aPOSTString) {
  1395.     this._logger.trace("_sendRequest");
  1396.  
  1397.     let request =
  1398.       Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
  1399.  
  1400.     // add event handlers.
  1401.     request.QueryInterface(Ci.nsIDOMEventTarget);
  1402.  
  1403.     if (null != aLoadHandler) {
  1404.       request.addEventListener("load", aLoadHandler, false);
  1405.     }
  1406.  
  1407.     if (null != aErrorHandler) {
  1408.       request.addEventListener("error", aErrorHandler, false);
  1409.     }
  1410.  
  1411.     // prepare and send the request.
  1412.     request.QueryInterface(Ci.nsIXMLHttpRequest);
  1413.     request.open((aIsPOST ? "POST" : "GET"), aURL, true);
  1414.  
  1415.     if (null != aHeaders) {
  1416.       for (let header in aHeaders) {
  1417.         request.setRequestHeader(header, aHeaders[header]);
  1418.       }
  1419.     }
  1420.  
  1421.     if (aIsPOST) {
  1422.       request.send(aPOSTString);
  1423.     } else {
  1424.       request.send(null);
  1425.     }
  1426.   },
  1427.  
  1428.   /**
  1429.    * Converts the given string into a UTF-8 string that can be sent through POST
  1430.    * as if it were binary. This is required for several Last.fm calls.
  1431.    * @param aString the string to convert into a stream.
  1432.    * @return nsIInputStream for the given string.
  1433.    */
  1434.   _convertToStream : function(aString) {
  1435.     this._logger.trace("_convertToStream");
  1436.  
  1437.     let multiStream =
  1438.       Cc["@mozilla.org/io/multiplex-input-stream;1"].
  1439.         createInstance(Ci.nsIMultiplexInputStream);
  1440.     let converter =
  1441.       Cc["@mozilla.org/intl/scriptableunicodeconverter"].
  1442.         createInstance(Ci.nsIScriptableUnicodeConverter);
  1443.     let inputStream;
  1444.  
  1445.     converter.charset = "UTF-8";
  1446.     inputStream = converter.convertToInputStream(aString);
  1447.     multiStream.appendStream(inputStream);
  1448.  
  1449.     return multiStream;
  1450.   },
  1451.  
  1452.   /**
  1453.    * Default error callback handler for the asynchronous requests.
  1454.    * @param aSource a string that identifies the source of the error.
  1455.    * @param aEvent the event that triggered this function.
  1456.    */
  1457.   _defaultError : function(aSource, aEvent) {
  1458.     this._logger.debug("_defaultError");
  1459.  
  1460.     try {
  1461.       this._logger.error(
  1462.         "_defaultError. Source: " + aSource + ", status: " +
  1463.         aEvent.target.status + ", response: " + aEvent.target.responseText);
  1464.     } catch (e) {
  1465.       this._logger.error("_defaultError. Error:\n" + e);
  1466.     }
  1467.   },
  1468.  
  1469.   /**
  1470.    * FUEL event handler. We use it to listen to changes to the Scrobble
  1471.    * preference.
  1472.    * @param aEvent the event that triggered this function.
  1473.    */
  1474.   handleEvent : function(aEvent) {
  1475.     this._logger.debug("handleEvent");
  1476.     this._scrobbleActive = this._scrobblePref.value;
  1477.  
  1478.     if (this._scrobbleActive && (null != FireFM.Login.userName)) {
  1479.       this._sendScrobbleHandshake(false);
  1480.     }
  1481.   },
  1482.  
  1483.   /**
  1484.    * Observes notifications of cookie and track activity.
  1485.    * @param aSubject The object that experienced the change.
  1486.    * @param aTopic The topic being observed.
  1487.    * @param aData The data related to the change.
  1488.    */
  1489.   observe : function(aSubject, aTopic, aData) {
  1490.     // XXX: there is no logging here for performance purposes.
  1491.     switch (aTopic) {
  1492.       case FireFM.Player.TOPIC_TRACK_LOADED:
  1493.         this.scrobbleTrack();
  1494.         this._toBeScrobbled = null;
  1495.         this._loved = false;
  1496.         this._banned = false;
  1497.         this._skipped = false;
  1498.         this._sendNowPlaying(aSubject.wrappedJSObject);
  1499.         break;
  1500.       case FireFM.Player.TOPIC_PROGRESS_CHANGED:
  1501.         let currentTrack = FireFM.Playlist.currentTrack;
  1502.  
  1503.         // check that Scrobbling is active, the track has not already been
  1504.         // marked to be Scrobbled, that the duration of the track is at least
  1505.         // 30 seconds, and that the track can be Scrobbled depending on its
  1506.         // progress.
  1507.         if (this._scrobbleActive && (null != this._scrobbleSessionId) &&
  1508.             (null == this._toBeScrobbled) &&
  1509.             (SCROBBLE_MIN_DURATION <= currentTrack.duration) &&
  1510.             this._canScrobble(currentTrack, aData)) {
  1511.           this._toBeScrobbled = currentTrack;
  1512.           this._logger.debug("observe. This track will be scrobbled.");
  1513.         }
  1514.  
  1515.         break;
  1516.     }
  1517.   }
  1518. };
  1519.  
  1520. /**
  1521.  * FireFM.Remote constructor.
  1522.  */
  1523. (function() {
  1524.   this.init();
  1525. }).apply(FireFM.Remote);
  1526.